Skip to content

Conversation

@steakhal
Copy link
Contributor

@steakhal steakhal commented Nov 20, 2025

BugSuppression works by traversing the lexical decl context of the decl-with-issue to record what source ranges should be suppressed by some attribute.
Note that the decl-with-issue will be changed to the lexical decl context of the original decl-with-issue, to make suppression attributes work that were attached to the CXXRecordDecl containing the CXXMethodDecl (bug report's DeclWithIssue).

It happens so that it uses a DynamicRecursiveASTVisitor, which has a couple of traversal options. Namely:

  • ShouldVisitTemplateInstantiations
  • ShouldWalkTypesOfTypeLocs
  • ShouldVisitImplicitCode
  • ShouldVisitLambdaBody

By default, these have the correct values, except for ShouldVisitTemplateInstantiations. We should traverse template instantiations because that might be where the bug is reported - thus, where we might have a [[clang::suppress]] that we should honor.

In this patch I'll explicitly set these traversal options to avoid further confusion.

rdar://164646398

BugSuppression works by traversing the lexical decl context of the decl
with issue to record what source ranges should be suppressed by some
attribute.

It happens so that it uses a DynamicRecursiveASTVisitor, which has a
couple of traversal options. Namely:

 - ShouldVisitTemplateInstantiations
 - ShouldWalkTypesOfTypeLocs
 - ShouldVisitImplicitCode
 - ShouldVisitLambdaBody

By default, these have the correct values, except for ShouldVisitTemplateInstantiations.
We should traverse template instantiations because that might be where
the bug is reported - thus, where we might have a [[clang::suppress]]
that we should honor.

In this patch I'll explicitly set these traversal options to avoid
further confusion.

rdar://164646398
@llvmbot llvmbot added the clang Clang issues not falling into any other category label Nov 20, 2025
@llvmbot
Copy link
Member

llvmbot commented Nov 20, 2025

@llvm/pr-subscribers-clang

@llvm/pr-subscribers-clang-static-analyzer-1

Author: Balázs Benics (steakhal)

Changes

BugSuppression works by traversing the lexical decl context of the decl with issue to record what source ranges should be suppressed by some attribute.

It happens so that it uses a DynamicRecursiveASTVisitor, which has a couple of traversal options. Namely:

  • ShouldVisitTemplateInstantiations
  • ShouldWalkTypesOfTypeLocs
  • ShouldVisitImplicitCode
  • ShouldVisitLambdaBody

By default, these have the correct values, except for ShouldVisitTemplateInstantiations. We should traverse template instantiations because that might be where the bug is reported - thus, where we might have a [[clang::suppress]] that we should honor.

In this patch I'll explicitly set these traversal options to avoid further confusion.

rdar://164646398


Full diff: https://github.com/llvm/llvm-project/pull/168954.diff

2 Files Affected:

  • (modified) clang/lib/StaticAnalyzer/Core/BugSuppression.cpp (+6-1)
  • (modified) clang/test/Analysis/suppression-attr.cpp (+24-1)
diff --git a/clang/lib/StaticAnalyzer/Core/BugSuppression.cpp b/clang/lib/StaticAnalyzer/Core/BugSuppression.cpp
index 5b5f9df9cb0dc..a7300b7183590 100644
--- a/clang/lib/StaticAnalyzer/Core/BugSuppression.cpp
+++ b/clang/lib/StaticAnalyzer/Core/BugSuppression.cpp
@@ -117,7 +117,12 @@ class CacheInitializer : public DynamicRecursiveASTVisitor {
     }
   }
 
-  CacheInitializer(Ranges &R) : Result(R) {}
+  CacheInitializer(Ranges &R) : Result(R) {
+    ShouldVisitTemplateInstantiations = true;
+    ShouldWalkTypesOfTypeLocs = false;
+    ShouldVisitImplicitCode = false;
+    ShouldVisitLambdaBody = true;
+  }
   Ranges &Result;
 };
 
diff --git a/clang/test/Analysis/suppression-attr.cpp b/clang/test/Analysis/suppression-attr.cpp
index 89bc3c47dbd51..9ba56d976fddb 100644
--- a/clang/test/Analysis/suppression-attr.cpp
+++ b/clang/test/Analysis/suppression-attr.cpp
@@ -1,4 +1,27 @@
-// RUN: %clang_analyze_cc1 -analyzer-checker=core -verify %s
+// RUN: %clang_analyze_cc1 -analyzer-checker=core,debug.ExprInspection -verify %s
+
+void clang_analyzer_warnIfReached();
+
+struct Clazz {
+  template <typename T>
+  static void templated_memfn();
+};
+
+// This must come before the 'templated_memfn' is defined!
+static void instantiate() {
+  Clazz::templated_memfn<int>();
+}
+
+template <typename T>
+void Clazz::templated_memfn() {
+  // When we report a bug in a function, we traverse the lexical decl context
+  // of it while looking for suppression attributes to record what source
+  // ranges should the suppression apply to.
+  // In the past, that traversal didn't follow template instantiations, only
+  // primary templates.
+  [[clang::suppress]] clang_analyzer_warnIfReached(); // no-warning
+
+}
 
 namespace [[clang::suppress]]
 suppressed_namespace {

@github-actions
Copy link

🐧 Linux x64 Test Results

  • 111350 tests passed
  • 4431 tests skipped

Copy link
Contributor

@ziqingluo-90 ziqingluo-90 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix LGTM.
I have one question for learning about how BugSuppression works though.

@steakhal
Copy link
Contributor Author

steakhal commented Nov 21, 2025

Thank you for the review @ziqingluo-90. I'll adjust the commit message to reflect on your question before landing the change.

EDIT: enhanced the PR summary. I plan to merge the PR tomorrow unless someone objects.

Copy link
Collaborator

@haoNoQ haoNoQ left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm somewhat surprised that it's necessary to visit template instantiations. This machine simply memorizes source locations. Template instantiations have the same source locations as the template itself right? (...right? 😳)

LGTM because I'm sure there's a perfectly reasonable explanation for that.

@steakhal
Copy link
Contributor Author

I'm somewhat surprised that it's necessary to visit template instantiations. This machine simply memorizes source locations. Template instantiations have the same source locations as the template itself right? (...right? 😳)

LGTM because I'm sure there's a perfectly reasonable explanation for that.

I see what you mean. I'll have a look tomorrow.
Thanks for the reviews!

@steakhal
Copy link
Contributor Author

I'm somewhat surprised that it's necessary to visit template instantiations. This machine simply memorizes source locations. Template instantiations have the same source locations as the template itself right? (...right? 😳)
LGTM because I'm sure there's a perfectly reasonable explanation for that.

I see what you mean. I'll have a look tomorrow. Thanks for the reviews!

It actually didn't take long. Check the AST of the example: https://godbolt.org/z/MsT5jx3nE

Funnily, the primary template is outside of the CXXRecordDecl. Only the template instantiation is under the CXXRecordDecl.
That should explain the traversal. IDK why don't we have a primary template within the class. Maybe because there can only be exactly 1 primary template. And that is "spelled" outside of the class declaration, thus to remain truthful to the sources, then the primary template AST must also live where it's spelled. I think this is the explanation.
Yeah, there are days when I hate C++, if you ask me.

@haoNoQ
Copy link
Collaborator

haoNoQ commented Nov 22, 2025

IDK why don't we have a primary template within the class.

Ohhh. Yeah I'm assuming that this is because compiling uninstantiated templates is so difficult that you can't even tell whether that template actually belongs inside the class at this stage of compilation. You only learn that after substituting the parameter.

(I may be completely wrong but this is the usual explanation for these quirks IIRC.)

(There's no way to put different instantiations in different classes through something like

template <typename T>
void T::foo(){}

right? I hope it's just a simplification that the compilation process does simply because it can.)

@haoNoQ
Copy link
Collaborator

haoNoQ commented Nov 22, 2025

On the other hand, I think I know why this wasn't a problem before. It wasn't a problem before because most of the time method templates (or methods of class templates) aren't written out of line. This is because you have to keep them in the header anyway because otherwise you won't be able to instantiate them. So might as well write inline.

Which means that there's probably a small opportunity for optimization there. It may be a good idea to include the uninstantiated template in the scan range while continuing to skip all instantiations? Because that's presumably how everything else works anyway. And this way we won't need to scan all the instantiations that we still don't need to scan. And this could be, like, somewhat noticeable for performance, I think.

@steakhal
Copy link
Contributor Author

Which means that there's probably a small opportunity for optimization there. It may be a good idea to include the uninstantiated template in the scan range while continuing to skip all instantiations? Because that's presumably how everything else works anyway. And this way we won't need to scan all the instantiations that we still don't need to scan. And this could be, like, somewhat noticeable for performance, I think.

Then I think explicit template specializations would break. The unfortunate fact is that primary templates are not that useful for us.

To sidestep this, what if we would not overrule the original DeclWithIssue and just traverse that one. That will be the correct fn to traverse.
Besides that, would need to retrofit the lexical scope behavior, which is (if we can trust the comment) about enclosing suppress attributes attached to the parent CXXRecordDecls.
If that was the only motivation there, then its easy to just check exactly that.

But honestly, I just probably wouldnt touch this.

@haoNoQ
Copy link
Collaborator

haoNoQ commented Nov 22, 2025

It's actually much weirder than that. Your example only causes problems when the implicit instantiation is requested *above* the definition. And that's also a prerequisite for the quirky AST. Compare: https://godbolt.org/z/4eT8Er3ev

So I think explicit specializations are actually fine because it's an error to perform them after an implicit instantiation: https://godbolt.org/z/ddT4bffvK

@steakhal
Copy link
Contributor Author

Yeah, thanks for looking at this again.
So, are we still good with this patch?

@haoNoQ
Copy link
Collaborator

haoNoQ commented Nov 22, 2025

Yeah I think this is good to go.

I think it'd be a better solution to use the information that we've already gathered, instead of mining more information. But that's most likely a non-issue.

@steakhal steakhal merged commit a088e74 into llvm:main Nov 23, 2025
13 checks passed
@steakhal steakhal deleted the bb/rdar-164646398-suppress-tmpl-mfns branch November 23, 2025 09:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

clang:static analyzer clang Clang issues not falling into any other category

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants